package org.hackystat.sensor.ant.emma;

import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import javax.xml.datatype.XMLGregorianCalendar;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.hackystat.sensor.ant.emma.resource.jaxb.All;
import org.hackystat.sensor.ant.emma.resource.jaxb.Class;
import org.hackystat.sensor.ant.emma.resource.jaxb.Coverage;
import org.hackystat.sensor.ant.emma.resource.jaxb.Data;
import org.hackystat.sensor.ant.emma.resource.jaxb.Package;
import org.hackystat.sensor.ant.emma.resource.jaxb.Report;
import org.hackystat.sensor.ant.emma.resource.jaxb.Srcfile;
import org.hackystat.sensor.ant.util.JavaClass2FilePathMapper;
import org.hackystat.sensor.ant.util.LongTimeConverter;
import org.hackystat.sensorshell.SensorProperties;
import org.hackystat.sensorshell.SensorPropertiesException;
import org.hackystat.sensorshell.SensorShell;
import org.hackystat.sensorshell.usermap.SensorShellMap;
import org.hackystat.sensorshell.usermap.SensorShellMapException;
import org.hackystat.utilities.tstamp.TstampSet;

/**
 * Implements an Ant task that parses the XML files generated by Emma a Java coverage tool.
 * Ant Task and sends the Coverage data to a Hackystat server.
 *
 * @author Aaron A. Kagawa, Cedric Qin Zhang
 * @version $Id$
 */
public class EmmaSensor extends Task {

  /** A list of all XML file sets generated by the Emma task. */
  private String emmaReportXmlFile;
  
  /** A list of file sets for Java source files. */
  private ArrayList<FileSet> javaSourceFilesets = new ArrayList<FileSet>();
  
  /** Whether or not to print out messages during sensor execution. */
  private boolean verbose = false;
  
  /** Timestamp set that will guarantee uniqueness of Emma timestamps. */
  private TstampSet tstampSet;
  /** Tool in UserMap to use. */
  private String tool;
  /** Tool account in the UserMap to use. */
  private String toolAccount;
  /** Sensor properties to be used with the sensor. */
  private SensorProperties sensorProps;
  /** The sensor shell instance used by this sensor. */
  private SensorShell sensorShell;
  /** The mapper used to map class names to file paths. */
  private JavaClass2FilePathMapper javaClass2FilePathMapper;
  
  /** Initialize a new instance of a EmmaSensor. */
  public EmmaSensor() {
    this.tstampSet = new TstampSet();
  }

  /**
   * Initialize a new instance of a EmmaSensor, passing the host and directory 
   *   key in explicitly. This supports testing. Note that when this constructor 
   *   is called, offline data recovery by the sensor is disabled.
   * @param host The hackystat host URL.
   * @param email The Hackystat email to use.
   * @param password The Hackystat password to use.
   */
  public EmmaSensor(String host, String email, String password) {
    // set sensorshell right away!
    this.sensorProps = new SensorProperties(host, email, password);
    this.sensorShell = new SensorShell(this.sensorProps, false, "test", false);
    this.tstampSet = new TstampSet();
  }
  
  /**
   * Set the verbose attribute to "on", "true", or "yes" to enable trace messages
   *   while the Emma sensor is running.
   * @param mode The new verbose value: should be "on", "true", or "yes" to enable.
   */
  public void setVerbose(String mode) {
    this.verbose = Project.toBoolean(mode);
  }
  
  /**
   * Allows the user to specify the tool in the UserMap that should be used when sending data. Note
   * that setting the tool will only have an effect if the tool account is also specified. Otherwise
   * it will be ignored and the values in v8.sensor.properties will be used.
   * 
   * @param tool The tool containing the tool account to be used when sending data.
   */
  public void setUserMapTool(String tool) {
    this.tool = tool;
  }

  /**
   * Allows the user to specify the tool account in the UserMap under the given tool to use when
   * sending data. Note that setting the tool account will only have an effect if the tool is also
   * specified. Otherwise the tool account will be ignored and v8.sensor.properties file values will
   * be used.
   * 
   * @param toolAccount The tool account in the UserMap to use when sending data.
   */
  public void setUserMapToolAccount(String toolAccount) {
    this.toolAccount = toolAccount;
  }

  /**
   * Sets the file path of the emma report xml file.
   * @param filePath The emma report file path.
   */
  public void setEmmaReportXmlFile(String filePath) {
    this.emmaReportXmlFile = filePath;
  }
  /**
   * Add a file set which contains java source files. This is used to map java class name
   * in emma report file back to its original java source file.
   *
   * @param fs The file set.
   */
  public void addFileSet(FileSet fs) {
    this.javaSourceFilesets.add(fs);
  }
  
  /**
   * Sets up the sensorshell instance to use either based on the given tool & tool account or from
   * the sensor.properties file. DO NOT call this method in the constructor. The optional properties
   * tool and tool account do not get set until after the constructor is done.
   */
  private void setupSensorShell() {
    if (isUsingUserMap()) {
      try {
        SensorShellMap map = new SensorShellMap(this.tool);
        this.sensorShell = map.getUserShell(this.toolAccount);
      }
      catch (SensorShellMapException e) {
        throw new BuildException(e.getMessage(), e);
      }
    }
    // sanity check to make sure the prop and shell haven't already been set by the
    // constructor that takes in the email, password, and host
    else if (this.sensorProps == null && this.sensorShell == null) {
      // use the sensor.properties file
      try {
        this.sensorProps = new SensorProperties();
        this.sensorShell = new SensorShell(this.sensorProps, false, "Emma");
      }
      catch (SensorPropertiesException e) {
        System.out.println(e.getMessage());
        System.out.println("Exiting...");
        throw new BuildException(e.getMessage(), e);
      }

      if (!this.sensorProps.isFileAvailable()) {
        System.out.println("Could not find sensor.properties file. ");
        System.out.println("Expected in: " + this.sensorProps.getAbsolutePath());
      }
    }
  }

  /**
   * Gets whether or not this sensor instance is using a mapping in the UserMap.
   * 
   * @return Returns true of the tool and tool account are set, otherwise false.
   */
  private boolean isUsingUserMap() {
    return (this.tool != null && this.toolAccount != null);
  }

  /**
   * Sends any accumulated data in the SensorShell to the server.
   * 
   * @return Returns the number of SensorData instances sent to the server.
   */
  public int send() {
    return this.sensorShell.send();
  }

  /**
   * Parses the Coverage XML files and sends the resulting coverage results to
   *   the hackystat server. This method is invoked automatically by Ant.
   * @throws BuildException If there is an error.
   */
  public void execute() throws BuildException {
    this.setupSensorShell();

    int numberOfEntries = 0;
    Date startTime = new Date();
    if (this.verbose) {
      System.out.println("Processing emma report file: " + this.emmaReportXmlFile);
    }
    try {
      numberOfEntries += this.processCoverageXmlFile(this.emmaReportXmlFile);
    }
    catch (Exception e) {
      System.out.println("Errors occurred while processing the coverage xml file: " + e);
    }

    if (this.send() > 0) {
      Date endTime = new Date();
      long elapsedTime = (endTime.getTime() - startTime.getTime()) / 1000;
      System.out.println("Hackystat data on " + numberOfEntries + " coverage entries sent to " 
          + this.sensorProps.getHackystatHost() + " (" + elapsedTime + " secs.)");
    }
    else {
      System.out.println("Failed to send Hackystat Coverage data. See log for details.");
    }
  }

  /**
   * Parses an Emma XML file and sends the data to the shell. The only coverage information that 
   * is used by the sensor is the Emma class level report. All other coverage information is 
   * ignored; for example the sensor does not use the method element coverage information. 
   * Instead, the sensor parses the class element. Here is an example: 
   * <pre>
   * <class name="JUnitSensor">
   *   <coverage type="class, %" value="100% (1/1)"/>
   *   <coverage type="method, %" value="58%  (7/12)"/>
   *   <coverage type="block, %" value="57%  (374/656)"/>
   *   <coverage type="line, %" value="58%  (80.5/139)"/>
   * </class>
   * </pre> 
   * The granularities of class, method, block, and line are retrieved from the class element. 
   * One could dig down into the method elements, but we are not doing this at the moment.  
   * 
   * @param fileNameString The XML file name to be processed.
   * @exception BuildException if any error.
   * @return The number of coverage entries in this XML file.
   */
  public int processCoverageXmlFile(String fileNameString) throws BuildException {
    File xmlFile = new File(fileNameString);
    // The start time for all entries will be approximated by the XML file's last mod time.
    // The shell will ensure that it's unique by tweaking the millisecond field.
    long startTime = xmlFile.lastModified();


    try {
      JAXBContext context = 
        JAXBContext.newInstance(org.hackystat.sensor.ant.emma.resource.jaxb.ObjectFactory.class);
      Unmarshaller unmarshaller = context.createUnmarshaller();
      
      // emma report
      Report report = (Report) unmarshaller.unmarshal(xmlFile);
      Data data = report.getData();
      All allData = data.getAll();
      
      int coverageEntriesCount = 0;
      for (Package packageReport : allData.getPackage()) {
        String packageName = packageReport.getName();
        for (Srcfile srcfile : packageReport.getSrcfile()) {
          for (Class classReport : srcfile.getClazz()) {
            String className = classReport.getName();
            for (Coverage coverage : classReport.getCoverage()) {
              String type = coverage.getType();
              String granularity = type.substring(0, type.indexOf(", %"));
              String value = coverage.getValue();
              String coveredString = value.substring(value.indexOf('(') + 1, value.indexOf('/'));
              String totalString = value.substring(value.indexOf('/') + 1, value.indexOf(')'));
              double covered = new Double(coveredString); 
              double total = new Double(totalString);

              String javaClassName = packageName + '.' + className;
              String javaSourceFilePath = 
                this.getJavaClass2FilePathMapper().getFilePath(javaClassName);
              if (javaSourceFilePath == null) {
                if (this.verbose) {
                  System.out.println("Warning: Unable to find java source file path for class '" 
                      + javaClassName + "'. Use empty string for file path.");
                }
                javaSourceFilePath = "";
              }
              
              // Alter startTime to guarantee uniqueness.
              long uniqueTstamp = this.tstampSet.getUniqueTstamp(startTime);

              // Get altered start time as XMLGregorianCalendar
              XMLGregorianCalendar startTimeGregorian = 
                LongTimeConverter.convertLongToGregorian(uniqueTstamp);

              Map<String, String> keyValMap = new HashMap<String, String>();
              keyValMap.put("Tool", "Emma");
              keyValMap.put("SensorDataType", "Coverage");
              keyValMap.put("DevEvent-Type", "Test");

              // Required
              keyValMap.put("Timestamp", startTimeGregorian.toString());
              keyValMap.put("Granularity", granularity);
              keyValMap.put("Resource", javaSourceFilePath);
              keyValMap.put("Covered", String.valueOf(covered));
              keyValMap.put("Uncovered", String.valueOf(total - covered));

              // Optional
              keyValMap.put("ClassName", javaClassName);
              
              this.sensorShell.add(keyValMap); // add data to sensorshell
              
              coverageEntriesCount++;
            }
          }
        }
      }
      
      return coverageEntriesCount;
    }
    catch (Exception e) {
      throw new BuildException("Failed to process " + fileNameString + "   " + e);
    }
  }

  /**
   * Get a java class to file path mapper.
   * @return The mapper.
   */
  private JavaClass2FilePathMapper getJavaClass2FilePathMapper() {
    if (this.javaClass2FilePathMapper == null) {
      List<String> fileList = new ArrayList<String>();
      for (FileSet fileSet : this.javaSourceFilesets) {
        DirectoryScanner ds = fileSet.getDirectoryScanner(this.getProject());
        ds.scan();
        String[] pathNames = ds.getIncludedFiles();
  
        for (int j = 0; j < pathNames.length; j++) {
          String pathname = pathNames[j];
          File file = new File(ds.getBasedir(), pathname);
          file = this.getProject().resolveFile(file.getPath());
          fileList.add(file.getAbsolutePath());
        }
      }
      this.javaClass2FilePathMapper = new JavaClass2FilePathMapper(fileList);
    }
    return this.javaClass2FilePathMapper;
  }
  
}